package com.bitbakery.plugin.cl.lang.parser;

import static com.bitbakery.plugin.cl.CLConstants.*;
import com.bitbakery.plugin.cl.lang.lexer.CLTokenTypes;
import static com.bitbakery.plugin.cl.lang.lexer.CLTokenTypes.*;
import static com.bitbakery.plugin.cl.lang.parser.CLDefmacroParser.parseDefmacro;
import static com.bitbakery.plugin.cl.lang.parser.CLDefunParser.parseDefun;
import static com.bitbakery.plugin.cl.lang.parser.CLDefunParser.parseLambda;
import static com.bitbakery.plugin.cl.lang.parser.CLStatementParser.parseFunctionCall;
import static com.bitbakery.plugin.cl.lang.psi.CLElementTypes.*;
import com.intellij.lang.ASTNode;
import com.intellij.lang.PsiBuilder;
import com.intellij.lang.PsiParser;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.NotNull;

public class CLParser implements PsiParser {
    //private static final Logger LOG = Logger.getInstance("#com.bitbakery.plugin.cl.lang.parser.CLParser");

    @NotNull
    public ASTNode parse(IElementType root, PsiBuilder builder) {
        final PsiBuilder.Marker rootMarker = builder.mark();
        while (!builder.eof()) {
            parseStatement(builder);
        }
        rootMarker.done(root);
        ASTNode tree = builder.getTreeBuilt();
        dump(tree);
        return tree;
    }

    protected static void parseStatement(PsiBuilder builder) {
        // Check for the end of the token stream
        if (builder.getTokenType() == null) {
            return;
        }

        // A literal all by itself is a valid CL statement
        if (builder.getTokenType() == NUMERIC_LITERAL || builder.getTokenType() == STRING_LITERAL
                || builder.getTokenType() == SYMBOL) { // TODO - Is this OK...? In general, are orphan symbols OK?
            advanceLexer(builder);
            return;
        }

        PsiBuilder.Marker marker = builder.mark();

        // TODO - We aren't correctly handling the case where an orphaned symbol is all by itself...

        // Check for empty or malformed statements
        if (builder.getTokenType() == BACKQUOTE) {
            advanceLexer(builder);
            parseStatement(builder);
            marker.done(BACKQUOTED_EXPRESSION);
            return;
        } else if (builder.getTokenType() == COMMA) {
            // TODO - comma statements are only allowed within backquote statements! (I think...)
            advanceLexer(builder);
            parseStatement(builder);
            marker.done(COMMA);
            return;
        } else if (builder.getTokenType() == COMMA_AT) {
            // TODO - comma-at statements are only allowed within backquote statements! (I think...)
            advanceLexer(builder);
            parseStatement(builder);
            marker.done(COMMA_AT);
            return;
        } else if (builder.getTokenType() != LEFT_PAREN) {
            builder.error(ERROR_EXPECTED_LEFT_PAREN);
            if (builder.getTokenType() == RIGHT_PAREN) {
                marker.drop();
                advanceLexer(builder);
                return;
            }
        } else {
            advanceLexer(builder);
        }

        // Make sure the statement doesn't end prematurely
        if (builder.getTokenType() == null) {
            marker.drop();
            builder.error(ERROR_EXPECTED_RIGHT_PAREN);
            return;
        }

        // We're now past the opening paren. Figure out our form, and parse accordingly
        if (builder.getTokenType() == DEFVAR) {
            parseDefvar(builder);
            marker.done(GLOBAL_VARIABLE_DEFINITION);
        } else if (builder.getTokenType() == DEFCONSTANT) {
            parseDefconstant(builder);
            marker.done(GLOBAL_CONSTANT_DEFINITION);
        } else if (builder.getTokenType() == DEFUN) {
            parseDefun(builder);
            marker.done(FUNCTION_DEFINITION);
        } else if (builder.getTokenType() == LAMBDA) {
            parseLambda(builder);
            marker.done(ANONYMOUS_FUNCTION_DEFINITION);
        } else if (builder.getTokenType() == DEFMACRO) {
            parseDefmacro(builder);
            marker.done(MACRO_DEFINITION);
        } else {
            parseFunctionCall(builder);
            marker.done(FUNCTION_CALL);
        }
    }

    private static void advanceLexer(PsiBuilder builder) {
        if (builder.getTokenType() != null) {
            builder.advanceLexer();
        }
    }

    /**
     * Input: Builder is pointing at "defvar" token
     * Output: Builder is pointing at the closing right paren
     */
    private static void parseDefvar(PsiBuilder builder) {
        builder.advanceLexer();

        if (builder.getTokenType() != CLTokenTypes.SYMBOL) {
            builder.error(ERROR_EXPECTED_SYMBOL);
        } else {
            PsiBuilder.Marker nameMarker = builder.mark();
            builder.advanceLexer();
            nameMarker.done(VARIABLE_NAME);
        }

        if (builder.getTokenType() == RIGHT_PAREN) {
            builder.error(ERROR_EXPECTED_VALUE);
        } else {
            // TODO - Handle function calls...? Or lambdas...? For now, just advance the lexer.
            builder.advanceLexer();
        }

        while (builder.getTokenType() != RIGHT_PAREN) {
            builder.error(ERROR_EXPECTED_ONE_VALUE);
            builder.advanceLexer();
        }

        // If possible, set us to the token immediately following this defvar
        advanceLexer(builder);
    }

    /**
     * Input: Builder is pointing at "defconstant" token
     * Output: Builder is pointing at the closing right paren
     */
    private static void parseDefconstant(PsiBuilder builder) {
        builder.advanceLexer();

        if (builder.getTokenType() != CLTokenTypes.SYMBOL) {
            builder.error(ERROR_EXPECTED_SYMBOL);
        } else {
            PsiBuilder.Marker nameMarker = builder.mark();
            builder.advanceLexer();
            nameMarker.done(VARIABLE_NAME);
        }

        if (builder.getTokenType() == RIGHT_PAREN) {
            builder.error(ERROR_EXPECTED_VALUE);
        } else {
            // TODO - Handle function calls...? Or lambdas...? For now, just advance the lexer.
            builder.advanceLexer();
        }

        while (builder.getTokenType() != RIGHT_PAREN) {
            builder.error(ERROR_EXPECTED_ONE_VALUE);
            builder.advanceLexer();
        }

        // If possible, set us to the token immediately following this defvar
        advanceLexer(builder);
    }

    private void dump(ASTNode node) {
        dump(node, 0);
    }

    private void dump(ASTNode node, int indentLevel) {
        StringBuffer out = new StringBuffer();
        for (int i = 0; i < indentLevel; i++) {
            out.append("\t");
        }

        out.append(node.getElementType().toString());
        if (node instanceof PsiErrorElement) {
            out.append(" : ").append(((PsiErrorElement) node).getErrorDescription());
        } else if (!StringUtil.isEmptyOrSpaces(node.getText())) {
            out.append(" : ").append(node.getText());
        }
        System.out.println(out.toString());

        for (ASTNode child : node.getChildren(null)) {
            dump(child, indentLevel + 1);
        }
    }
}
